{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": [],
      "authorship_tag": "ABX9TyM6XDBP8oqAaLL0GMT0mBj+",
      "include_colab_link": true
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "view-in-github",
        "colab_type": "text"
      },
      "source": [
        "<a href=\"https://colab.research.google.com/github/mshumer/OpenDeepResearcher/blob/main/open_deep_researcher_gradio.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "bSpd75_8O8rr"
      },
      "outputs": [],
      "source": [
        "!pip install nest_asyncio gradio aiohttp\n",
        "\n",
        "import nest_asyncio\n",
        "nest_asyncio.apply()\n",
        "\n",
        "import asyncio\n",
        "import aiohttp\n",
        "import gradio as gr\n",
        "import json\n",
        "\n",
        "# ---------------------------\n",
        "# Configuration Constants\n",
        "# ---------------------------\n",
        "OPENROUTER_API_KEY = \"REDACTED\" # Replace with your OpenRouter API key\n",
        "SERPAPI_API_KEY = \"REDACTED\" # Replace with your SERPAPI API key\n",
        "JINA_API_KEY = \"REDACTED\" # Replace with your Jina API key\n",
        "\n",
        "OPENROUTER_URL = \"https://openrouter.ai/api/v1/chat/completions\"\n",
        "SERPAPI_URL = \"https://serpapi.com/search\"\n",
        "JINA_BASE_URL = \"https://r.jina.ai/\"\n",
        "\n",
        "DEFAULT_MODEL = \"anthropic/claude-3.5-haiku\"\n",
        "\n",
        "# -------------------------------\n",
        "# Asynchronous Helper Functions\n",
        "# -------------------------------\n",
        "\n",
        "async def call_openrouter_async(session, messages, model=DEFAULT_MODEL):\n",
        "    headers = {\n",
        "        \"Authorization\": f\"Bearer {OPENROUTER_API_KEY}\",\n",
        "        \"X-Title\": \"OpenDeepResearcher, by Matt Shumer\",\n",
        "        \"Content-Type\": \"application/json\"\n",
        "    }\n",
        "    payload = {\n",
        "        \"model\": model,\n",
        "        \"messages\": messages\n",
        "    }\n",
        "    try:\n",
        "        async with session.post(OPENROUTER_URL, headers=headers, json=payload) as resp:\n",
        "            if resp.status == 200:\n",
        "                result = await resp.json()\n",
        "                try:\n",
        "                    return result['choices'][0]['message']['content']\n",
        "                except (KeyError, IndexError):\n",
        "                    print(\"Unexpected OpenRouter response structure:\", result)\n",
        "                    return None\n",
        "            else:\n",
        "                text = await resp.text()\n",
        "                print(f\"OpenRouter API error: {resp.status} - {text}\")\n",
        "                return None\n",
        "    except Exception as e:\n",
        "        print(\"Error calling OpenRouter:\", e)\n",
        "        return None\n",
        "\n",
        "async def generate_search_queries_async(session, user_query):\n",
        "    prompt = (\n",
        "        \"You are an expert research assistant. Given the user's query, generate up to four distinct, \"\n",
        "        \"precise search queries that would help gather complete information on the topic. \"\n",
        "        \"Return only a Python list of strings, for example: ['query1', 'query2', 'query3'].\"\n",
        "    )\n",
        "    messages = [\n",
        "        {\"role\": \"system\", \"content\": \"You are a helpful and precise research assistant.\"},\n",
        "        {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\n\\n{prompt}\"}\n",
        "    ]\n",
        "    response = await call_openrouter_async(session, messages)\n",
        "    if response:\n",
        "        try:\n",
        "            search_queries = eval(response)\n",
        "            if isinstance(search_queries, list):\n",
        "                return search_queries\n",
        "            else:\n",
        "                print(\"LLM did not return a list. Response:\", response)\n",
        "                return []\n",
        "        except Exception as e:\n",
        "            print(\"Error parsing search queries:\", e, \"\\nResponse:\", response)\n",
        "            return []\n",
        "    return []\n",
        "\n",
        "async def perform_search_async(session, query):\n",
        "    params = {\n",
        "        \"q\": query,\n",
        "        \"api_key\": SERPAPI_API_KEY,\n",
        "        \"engine\": \"google\"\n",
        "    }\n",
        "    try:\n",
        "        async with session.get(SERPAPI_URL, params=params) as resp:\n",
        "            if resp.status == 200:\n",
        "                results = await resp.json()\n",
        "                if \"organic_results\" in results:\n",
        "                    links = [item.get(\"link\") for item in results[\"organic_results\"] if \"link\" in item]\n",
        "                    return links\n",
        "                else:\n",
        "                    print(\"No organic results in SERPAPI response.\")\n",
        "                    return []\n",
        "            else:\n",
        "                text = await resp.text()\n",
        "                print(f\"SERPAPI error: {resp.status} - {text}\")\n",
        "                return []\n",
        "    except Exception as e:\n",
        "        print(\"Error performing SERPAPI search:\", e)\n",
        "        return []\n",
        "\n",
        "async def fetch_webpage_text_async(session, url):\n",
        "    full_url = f\"{JINA_BASE_URL}{url}\"\n",
        "    headers = {\n",
        "        \"Authorization\": f\"Bearer {JINA_API_KEY}\"\n",
        "    }\n",
        "    try:\n",
        "        async with session.get(full_url, headers=headers) as resp:\n",
        "            if resp.status == 200:\n",
        "                return await resp.text()\n",
        "            else:\n",
        "                text = await resp.text()\n",
        "                print(f\"Jina fetch error for {url}: {resp.status} - {text}\")\n",
        "                return \"\"\n",
        "    except Exception as e:\n",
        "        print(\"Error fetching webpage text with Jina:\", e)\n",
        "        return \"\"\n",
        "\n",
        "async def is_page_useful_async(session, user_query, page_text):\n",
        "    prompt = (\n",
        "        \"You are a critical research evaluator. Given the user's query and the content of a webpage, \"\n",
        "        \"determine if the webpage contains information that is useful for addressing the query. \"\n",
        "        \"Respond with exactly one word: 'Yes' if the page is useful, or 'No' if it is not. Do not include any extra text.\"\n",
        "    )\n",
        "    messages = [\n",
        "        {\"role\": \"system\", \"content\": \"You are a strict and concise evaluator of research relevance.\"},\n",
        "        {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\n\\nWebpage Content (first 20000 characters):\\n{page_text[:20000]}\\n\\n{prompt}\"}\n",
        "    ]\n",
        "    response = await call_openrouter_async(session, messages)\n",
        "    if response:\n",
        "        answer = response.strip()\n",
        "        if answer in [\"Yes\", \"No\"]:\n",
        "            return answer\n",
        "        else:\n",
        "            if \"Yes\" in answer:\n",
        "                return \"Yes\"\n",
        "            elif \"No\" in answer:\n",
        "                return \"No\"\n",
        "    return \"No\"\n",
        "\n",
        "async def extract_relevant_context_async(session, user_query, search_query, page_text):\n",
        "    prompt = (\n",
        "        \"You are an expert information extractor. Given the user's query, the search query that led to this page, \"\n",
        "        \"and the webpage content, extract all pieces of information that are useful for answering the user's query. \"\n",
        "        \"Return only the relevant context as plain text without extra commentary.\"\n",
        "    )\n",
        "    messages = [\n",
        "        {\"role\": \"system\", \"content\": \"You are an expert in extracting and summarizing relevant information.\"},\n",
        "        {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\nSearch Query: {search_query}\\n\\nWebpage Content (first 20000 characters):\\n{page_text[:20000]}\\n\\n{prompt}\"}\n",
        "    ]\n",
        "    response = await call_openrouter_async(session, messages)\n",
        "    if response:\n",
        "        return response.strip()\n",
        "    return \"\"\n",
        "\n",
        "async def get_new_search_queries_async(session, user_query, previous_search_queries, all_contexts):\n",
        "    context_combined = \"\\n\".join(all_contexts)\n",
        "    prompt = (\n",
        "        \"You are an analytical research assistant. Based on the original query, the search queries performed so far, \"\n",
        "        \"and the extracted contexts from webpages, decide if further research is needed. \"\n",
        "        \"If further research is needed, provide up to four new search queries as a Python list (for example, \"\n",
        "        \"['new query1', 'new query2']). If you believe no further research is needed, respond with exactly <done>.\"\n",
        "        \"\\nOutput only a Python list or the token <done> without any extra text.\"\n",
        "    )\n",
        "    messages = [\n",
        "        {\"role\": \"system\", \"content\": \"You are a systematic research planner.\"},\n",
        "        {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\nPrevious Search Queries: {previous_search_queries}\\n\\nExtracted Relevant Contexts:\\n{context_combined}\\n\\n{prompt}\"}\n",
        "    ]\n",
        "    response = await call_openrouter_async(session, messages)\n",
        "    if response:\n",
        "        cleaned = response.strip()\n",
        "        if cleaned == \"<done>\":\n",
        "            return \"<done>\"\n",
        "        try:\n",
        "            new_queries = eval(cleaned)\n",
        "            if isinstance(new_queries, list):\n",
        "                return new_queries\n",
        "            else:\n",
        "                print(\"LLM did not return a list for new search queries. Response:\", response)\n",
        "                return []\n",
        "        except Exception as e:\n",
        "            print(\"Error parsing new search queries:\", e, \"\\nResponse:\", response)\n",
        "            return []\n",
        "    return []\n",
        "\n",
        "async def generate_final_report_async(session, user_query, all_contexts):\n",
        "    context_combined = \"\\n\".join(all_contexts)\n",
        "    prompt = (\n",
        "        \"You are an expert researcher and report writer. Based on the gathered contexts below and the original query, \"\n",
        "        \"write a complete, well-structured, and detailed report that addresses the query thoroughly. \"\n",
        "        \"Include all useful insights and conclusions without extra commentary.\"\n",
        "    )\n",
        "    messages = [\n",
        "        {\"role\": \"system\", \"content\": \"You are a skilled report writer.\"},\n",
        "        {\"role\": \"user\", \"content\": f\"User Query: {user_query}\\n\\nGathered Relevant Contexts:\\n{context_combined}\\n\\n{prompt}\"}\n",
        "    ]\n",
        "    report = await call_openrouter_async(session, messages)\n",
        "    return report\n",
        "\n",
        "async def process_link(session, link, user_query, search_query, log):\n",
        "    log.append(f\"Fetching content from: {link}\")\n",
        "    page_text = await fetch_webpage_text_async(session, link)\n",
        "    if not page_text:\n",
        "        log.append(f\"Failed to fetch content from: {link}\")\n",
        "        return None\n",
        "    usefulness = await is_page_useful_async(session, user_query, page_text)\n",
        "    log.append(f\"Page usefulness for {link}: {usefulness}\")\n",
        "    if usefulness == \"Yes\":\n",
        "        context = await extract_relevant_context_async(session, user_query, search_query, page_text)\n",
        "        if context:\n",
        "            log.append(f\"Extracted context from {link} (first 200 chars): {context[:200]}\")\n",
        "            return context\n",
        "    return None\n",
        "\n",
        "# -----------------------------\n",
        "# Main Asynchronous Routine\n",
        "# -----------------------------\n",
        "\n",
        "async def async_research(user_query, iteration_limit):\n",
        "    aggregated_contexts = []\n",
        "    all_search_queries = []\n",
        "    log_messages = []  # List to store intermediate steps\n",
        "    iteration = 0\n",
        "\n",
        "    async with aiohttp.ClientSession() as session:\n",
        "        log_messages.append(\"Generating initial search queries...\")\n",
        "        new_search_queries = await generate_search_queries_async(session, user_query)\n",
        "        if not new_search_queries:\n",
        "            log_messages.append(\"No search queries were generated by the LLM. Exiting.\")\n",
        "            return \"No search queries were generated by the LLM. Exiting.\", \"\\n\".join(log_messages)\n",
        "        all_search_queries.extend(new_search_queries)\n",
        "        log_messages.append(f\"Initial search queries: {new_search_queries}\")\n",
        "\n",
        "        while iteration < iteration_limit:\n",
        "            log_messages.append(f\"\\n=== Iteration {iteration + 1} ===\")\n",
        "            iteration_contexts = []\n",
        "            search_tasks = [perform_search_async(session, query) for query in new_search_queries]\n",
        "            search_results = await asyncio.gather(*search_tasks)\n",
        "            unique_links = {}\n",
        "            for idx, links in enumerate(search_results):\n",
        "                query_used = new_search_queries[idx]\n",
        "                for link in links:\n",
        "                    if link not in unique_links:\n",
        "                        unique_links[link] = query_used\n",
        "\n",
        "            log_messages.append(f\"Aggregated {len(unique_links)} unique links from this iteration.\")\n",
        "            link_tasks = [\n",
        "                process_link(session, link, user_query, unique_links[link], log_messages)\n",
        "                for link in unique_links\n",
        "            ]\n",
        "            link_results = await asyncio.gather(*link_tasks)\n",
        "            for res in link_results:\n",
        "                if res:\n",
        "                    iteration_contexts.append(res)\n",
        "\n",
        "            if iteration_contexts:\n",
        "                aggregated_contexts.extend(iteration_contexts)\n",
        "                log_messages.append(f\"Found {len(iteration_contexts)} useful contexts in this iteration.\")\n",
        "            else:\n",
        "                log_messages.append(\"No useful contexts were found in this iteration.\")\n",
        "\n",
        "            new_search_queries = await get_new_search_queries_async(session, user_query, all_search_queries, aggregated_contexts)\n",
        "            if new_search_queries == \"<done>\":\n",
        "                log_messages.append(\"LLM indicated that no further research is needed.\")\n",
        "                break\n",
        "            elif new_search_queries:\n",
        "                log_messages.append(f\"LLM provided new search queries: {new_search_queries}\")\n",
        "                all_search_queries.extend(new_search_queries)\n",
        "            else:\n",
        "                log_messages.append(\"LLM did not provide any new search queries. Ending the loop.\")\n",
        "                break\n",
        "\n",
        "            iteration += 1\n",
        "\n",
        "        log_messages.append(\"\\nGenerating final report...\")\n",
        "        final_report = await generate_final_report_async(session, user_query, aggregated_contexts)\n",
        "        return final_report, \"\\n\".join(log_messages)\n",
        "\n",
        "def run_research(user_query, iteration_limit=10):\n",
        "    return asyncio.run(async_research(user_query, iteration_limit))\n",
        "\n",
        "# -----------------------------\n",
        "# Gradio UI Setup\n",
        "# -----------------------------\n",
        "\n",
        "def gradio_run(user_query, iteration_limit):\n",
        "    try:\n",
        "        final_report, logs = run_research(user_query, int(iteration_limit))\n",
        "        return final_report, logs\n",
        "    except Exception as e:\n",
        "        return f\"An error occurred: {e}\", \"\"\n",
        "\n",
        "iface = gr.Interface(\n",
        "    fn=gradio_run,\n",
        "    inputs=[\n",
        "        gr.Textbox(lines=2, label=\"Research Query/Topic\"),\n",
        "        gr.Number(value=10, label=\"Max Iterations\")\n",
        "    ],\n",
        "    outputs=[\n",
        "        gr.Textbox(label=\"Final Report\"),\n",
        "        gr.Textbox(label=\"Intermediate Steps Log\")\n",
        "    ],\n",
        "    title=\"Research Assistant\",\n",
        "    description=\"Enter your query and a maximum iteration count to generate a report. The log will show the steps taken.\"\n",
        ")\n",
        "\n",
        "iface.launch()"
      ]
    }
  ]
}